/*
 * The Yard Utilties - http://www.theyard.net/
 * 
 * Copyright (c) 2008 by Vlideshow, Inc..  All Rights Resrved.
 * 
 * This library is free software; you can redistribute it and/or modify it under the 
 * terms of the GNU Lesser General Public License as published by the Free Software 
 * Foundation; either version 2.1 of the License, or (at your option) any later 
 * version. 
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY 
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along 
 * with this library; if not, write to the Free Software Foundation, Inc., 
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 */
package net.theyard.components
{
  import flash.events.Event;

  /**
   * A collection of general purpose utilities for logging, debugging, etc.
   */
  public class Utils
  {
      private static var sDebugTextArea:Object=null;
      private static var sDebugTextMax:int=0;
      private static var enableHtml:Boolean = false;
      private static var debuggingEnabled:Boolean = false;

      /**
       * Set whether to output debugging information when ytrace is called.
       *
       * @param debuggingEnabled whether to output or not; default is true
       * @see #ytrace()
       */
      public static function setDebuggingEnabled(debuggingEnabled:Boolean):void
      {
        Utils.debuggingEnabled = debuggingEnabled;
      }

      /**
       * Set whether to pretty print in html the outputs from ytrace.
       *
       * @param htmlEnabled whether to output in HTML or not; default is true.
       * @see #ytrace()
       */
      public static function setHtmlEnabled(htmlEnabled:Boolean):void
      {
        Utils.enableHtml = htmlEnabled;
      }

      /**
       * Returns whether or not #ytrace(...) will output to the flashlog
       *
       * @return is debugging output enabled.
       * @see #ytrace()
       */
      public static function isDebugEnabled():Boolean
      {
        return debuggingEnabled;
      }

      /**
       * Tells #ytrace(...) about a text area it should output to.
       *
       * <p>
       * Use this method to pass in a Flash or Flex object that #ytrace(...)
       * should also echo output to.  The object you pass in requires the
       * following properties:
       * </p>
       * <p>htmlText: String [read/write]
       * </p>
       * <p>verticalScrollPosition: int [read/write]
       * </p>
       * <p>maxVerticalScrollPosition: int [read/write]
       * </p>
       *
       * @param aTextArea: The object to output to, or null to disable output.
       * @param maxSize: The maximum abmount of text to have in the htmlText element
       *   at any time.
       * @return The old setting
       * @see #ytrace()
       */
      public static function setDebugTextArea(
        aTextArea:Object, maxSize:int=-1):Object
      {
        var oldTextArea:Object = sDebugTextArea;
        sDebugTextArea = aTextArea;
        sDebugTextMax = maxSize;
        return oldTextArea;
      }

      /**
       * Similar to trace(...) but on steriods.
       *
       * This method calls trace(...) but does the following first:
       * <ul>
       *   <li>Adds the current time to the message.</li>
       *   <li>If running in a debug player with a debug build,
       *     prints the name of the calling method</li>
       *   <li>If setDebugTextArea() has been set, also outputs
       *     the message to that area.</li>
       * </ul>
       *
       * @param args The arguments to output
       * @see #setDebugTextArea()
       */
      public static function ytrace(... args):void
      {
        var msg:String="";
        var stack:String = null;

        var functionName:String = null;
        // IMPORTANT: Make sure this name corresponds to the correct class name
        // for ytrace
        const YTRACE_FUNCNAME:RegExp =
          /net.theyard.components::Utils\$\/ytrace()/;
        try
        {
          // We throw an error just to catch it and
          // get a stack trace.
          throw new Error;
        }
        catch (e:Error)
        {
          // Now, get a stack trace.
          stack = e.getStackTrace();
        }
        if (stack != null)
        {
          // Skip the Error tag at the starting line.
          if (stack.match(/Error/))
          {
            stack = stack.substr(stack.indexOf("\n")+1);
          }
          // Skip this function name if it exists by jumping
          // ahead one line.
          if (stack.match(YTRACE_FUNCNAME))
          {
            stack = stack.substr(stack.indexOf("\n")+1);
          }
          // delete the error at phrase
          stack = stack.replace(/\s*at /,"");
          // delete the filename between brackets
          stack = stack.replace(/\[.*\]/,"");
          // delete the class name if repeated before a /
          //stack = stack.replace(/.*\//,"");
          // and now find the remaining first line
          functionName = stack.match(/.*/)[0];
        }
        // insert the current time at the beginning of our trace

        if (enableHtml)
        {
          args.unshift("<b>");
          args.push("</b>");
        }
        args.unshift(yCurrTime() + " ");
        // and finally do the actual trace
        if (functionName != null && functionName.length > 0)
        {
          if (enableHtml)
            args.push(" <font color=\"#808080\">[" + functionName + "]</font>");
          else 
            args.push(" [" + functionName + "]");
        }

        msg = args.join("");
        trace(msg);

        // if debugging is not enabled then dont output to the debug text area

        if (!debuggingEnabled)
          return;

        updateDebugTextArea(msg);
      }

      private static function updateDebugTextArea(aMsg:String):void
      {
        if (sDebugTextArea != null && aMsg != null)
        {
          if (
            sDebugTextArea.htmlText != null && 
            sDebugTextMax != -1 &&
            sDebugTextArea.htmlText.length > sDebugTextMax - (aMsg.length + 1))
          {
            // Need to cull the status area

            sDebugTextArea.htmlText = sDebugTextArea.htmlText.substring(aMsg.length + 1);
          }
          sDebugTextArea.htmlText = (
            sDebugTextArea.htmlText.length > 0 
            ? sDebugTextArea.htmlText
            : "") + (aMsg  + "<br>");

          // scroll to bottom
          sDebugTextArea.verticalScrollPosition = 
            sDebugTextArea.maxVerticalScrollPosition + 1;
        }
      }
      /**
       * An function you can pass as an event handler that calls #ytrace(...) for any handled event.
       * @param event An event dispather will pass this event in.
       */
      public static function yGenericEventHandler(event:Event):void
      {
        ytrace(event);
      }

      /**
       * Returns the current time as a 24-hour clock of the form HH:MM:SS.
       *
       * Will zero-pad string as needed to maintain 8-character output.
       *
       * @return The current time.
       */
      public static function yCurrTime():String
      {
        //get date info from system
        var currDate:Date = new Date();
        var currHrs:String = (
          currDate.getHours() < 10 ? "0" : "") + currDate.getHours();
        var currMins:String = (
          currDate.getMinutes() < 10 ? "0" : "") + currDate.getMinutes();
        var currSecs:String = (
          currDate.getSeconds() < 10 ? "0" : "") + currDate.getSeconds();
        return currHrs + ":" + currMins + ":" + currSecs;
      }

      /**
       * Takes the contents of an associate array and outputs it as a string.
       *
       * @param aObject The associate array to output.
       * @return The formatted string.
       */
      public static function yAssocArrayAsString(aObject:Object):String
      {
        var retval:String="[";

        for (var field:String in aObject)
        {
          retval = retval + field + ": \"" + aObject[field] + "\";";
        }
        retval = retval + "]"
          return retval;
      }

      /**
       * Looks in associate array to ensure each key in the Keys array exists.
       *
       * @param aObject The associate array to search in.
       * @param aKeys   An array of keys (strings) to search for in aObject
       * @param aAllowEmptyStrings If false, then we assume all values
       * in aObject are strings, and raise an error if they are null or
       * "".  If true, we only raise an error on null.
       * @throws ArgumentError Throws this if a null array is passed, or if we don't allow empty strings but encounter an empty string.
       */
      public static function yValidateKeysInAssocArray(
        aObject:Object, aKeys:Array, aAllowEmptyStrings:Boolean=false):void
      {
        if (aObject == null)
        {
          throw new ArgumentError("Unexpected null associated array");
        }
        if (aKeys != null)
        {
          for (var i:int = 0; i < aKeys.length; i++ )
          {
            var key:String = aKeys[i] as String;
            var value:Object = (aObject[key]);
            if (value == null)
            {
              throw new ArgumentError("Missing expected key \"" + key + "\"");
            }
            else if (!aAllowEmptyStrings)
            {
              var valString:String = key as String;
              if (valString.length == 0)
              {
                throw new ArgumentError("Missing expected key \"" + key + "\"");
              }
            }
          }
        }
      }

      //public static function s(aFunc:Function):Function
      //{
      //  return function(... args):void {
      //    try
      //    {
      //      aFunc(args);
      //    }
      //    catch (e:Error)
      //    {
      //      trace("uncaught exception bubbled out of function");
      //      throw e;
      //    }
      //  }
      //}

      /*  sprintf(3) implementation in ActionScript 3.0.
       *
       *  Author:  Manish Jethani (manish.jethani@gmail.com)
       *  Date:    April 3, 2006
       *  Version: 0.1
       *
       *  Copyright (c) 2006 Manish Jethani
       *
       *  Permission is hereby granted, free of charge, to any person obtaining a
       *  copy of this software and associated documentation files (the "Software"),
       *  to deal in the Software without restriction, including without limitation
       *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
       *  and/or sell copies of the Software, and to permit persons to whom the
       *  Software is furnished to do so, subject to the following conditions: *
       *  The above copyright notice and this permission notice shall be included in
       *  all copies or substantial portions of the Software.
       *
       *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
       *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
       *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
       *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
       *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
       *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
       *  DEALINGS IN THE SOFTWARE.
       */

      /*  sprintf(3) implementation in ActionScript 3.0.
       *
       *  http://www.die.net/doc/linux/man/man3/sprintf.3.html
       *
       *  The following flags are supported: '#', '0', '-', '+'
       *
       *  Field widths are fully supported.  '*' is not supported.
       *
       *  Precision is supported except one difference from the standard: for an
       *  explicit precision of 0 and a result string of "0", the output is "0"
       *  instead of an empty string.
       *
       *  Length modifiers are not supported.
       *
       *  The following conversion specifiers are supported: 'd', 'i', 'o', 'u', 'x',
       *  'X', 'f', 'F', 'c', 's', '%'
       *
       *  Report bugs to manish.jethani@gmail.com
       */
      /**
       * Implements the C/C++ sprintf function.
       * @param format The format of args, in sprintf format
       * @param args as required by format
       *
       * @return a nicely formatted string.
       */
      public static function sprintf(format:String, ... args):String
      {
        var result:String = "";
        var next:*;
        var str:String;


        var length:int = format.length;
        for (var i:int = 0; i < length; i++)
        {
          var c:String = format.charAt(i);

          if (c == "%")
          {
            var pastFieldWidth:Boolean = false;
            var pastFlags:Boolean = false;

            var flagAlternateForm:Boolean = false;
            var flagZeroPad:Boolean = false;
            var flagLeftJustify:Boolean = false;
            var flagSpace:Boolean = false;
            var flagSign:Boolean = false;

            var fieldWidth:String = "";
            var precision:String = "";

            c = format.charAt(++i);

            while (c != "d"
            && c != "i"
            && c != "o"
            && c != "u"
            && c != "x"
            && c != "X"
            && c != "f"
            && c != "F"
            && c != "c"
            && c != "s"
            && c != "%")
            {
              if (!pastFlags)
              {
                if (!flagAlternateForm && c == "#")
                  flagAlternateForm = true;
                else if (!flagZeroPad && c == "0")
                  flagZeroPad = true;
                else if (!flagLeftJustify && c == "-")
                  flagLeftJustify = true;
                else if (!flagSpace && c == " ")
                  flagSpace = true;
                else if (!flagSign && c == "+")
                  flagSign = true;
                else
                  pastFlags = true;
              }

              if (!pastFieldWidth && c == ".")
              {
                pastFlags = true;
                pastFieldWidth = true;

                c = format.charAt(++i);
                continue;
              }

              if (pastFlags)
              {
                if (!pastFieldWidth)
                  fieldWidth += c;
                else
                  precision += c;
              }

              c = format.charAt(++i);
            }

            switch (c)
            {
              case "d":
              case "i":
                next = args.shift();
                str = String(Math.abs(int(next)));

                if (precision != "")
                  str = leftPad(str, int(precision), "0");

                if (int(next) < 0)
                  str = "-" + str;
                else if (flagSign && int(next) >= 0)
                  str = "+" + str;

                if (fieldWidth != "")
                {
                  if (flagLeftJustify)
                    str = rightPad(str, int(fieldWidth));
                  else if (flagZeroPad && precision == "")
                    str = leftPad(str, int(fieldWidth), "0");
                  else
                    str = leftPad(str, int(fieldWidth));
                }

                result += str;
                break;

              case "o":
                next = args.shift();
                str = uint(next).toString(8);

                if (flagAlternateForm && str != "0")
                  str = "0" + str;

                if (precision != "")
                  str = leftPad(str, int(precision), "0");

                if (fieldWidth != "")
                {
                  if (flagLeftJustify)
                    str = rightPad(str, int(fieldWidth));
                  else if (flagZeroPad && precision == "")
                    str = leftPad(str, int(fieldWidth), "0");
                  else
                    str = leftPad(str, int(fieldWidth));
                }

                result += str;
                break;

              case "u":
                next = args.shift();
                str = uint(next).toString(10);

                if (precision != "")
                  str = leftPad(str, int(precision), "0");

                if (fieldWidth != "")
                {
                  if (flagLeftJustify)
                    str = rightPad(str, int(fieldWidth));
                  else if (flagZeroPad && precision == "")
                    str = leftPad(str, int(fieldWidth), "0");
                  else
                    str = leftPad(str, int(fieldWidth));
                }

                result += str;
                break;

              case "X":
                var capitalise:
                Boolean = true;
              case "x":
                next = args.shift();
                str = uint(next).toString(16);

                if (precision != "")
                  str = leftPad(str, int(precision), "0");

                var prepend:
                Boolean = flagAlternateForm && uint(next) != 0;

                if (fieldWidth != "" && !flagLeftJustify
                && flagZeroPad && precision == "")
                  str = leftPad(str, prepend
                  ? int(fieldWidth) - 2 : int(fieldWidth), "0");

                if (prepend)
                  str = "0x" + str;

                if (fieldWidth != "")
                {
                  if (flagLeftJustify)
                    str = rightPad(str, int(fieldWidth));
                  else
                    str = leftPad(str, int(fieldWidth));
                }

                if (capitalise)
                  str = str.toUpperCase();

                result += str;
                break;

              case "f":
              case "F":
                next = args.shift();
                str = Math.abs(Number(next)).toFixed(
                  precision != "" ?  int(precision) : 6);

                if (int(next) < 0)
                  str = "-" + str;
                else if (flagSign && int(next) >= 0)
                  str = "+" + str;

                if (flagAlternateForm && str.indexOf(".") == -1)
                  str += ".";

                if (fieldWidth != "")
                {
                  if (flagLeftJustify)
                    str = rightPad(str, int(fieldWidth));
                  else if (flagZeroPad && precision == "")
                    str = leftPad(str, int(fieldWidth), "0");
                  else
                    str = leftPad(str, int(fieldWidth));
                }

                result += str;
                break;

              case "c":
                next = args.shift();
                str = String.fromCharCode(int(next));

                if (fieldWidth != "")
                {
                  if (flagLeftJustify)
                    str = rightPad(str, int(fieldWidth));
                  else
                    str = leftPad(str, int(fieldWidth));
                }

                result += str;
                break;

              case "s":
                next = args.shift();
                str = String(next);

                if (precision != "")
                  str = str.substring(0, int(precision));

                if (fieldWidth != "")
                {
                  if (flagLeftJustify)
                    str = rightPad(str, int(fieldWidth));
                  else
                    str = leftPad(str, int(fieldWidth));
                }

                result += str;
                break;

              case "%":
                result += "%";
            }
          }
          else
          {
            result += c;
          }
        }

        return result;
      }

      // Private functions

      private static function leftPad(source:String, targetLength:int, padChar:String = " "):String
      {
        if (source.length < targetLength)
        {
          var padding:String = "";

          while (padding.length + source.length < targetLength)
            padding += padChar;

          return padding + source;
        }

        return source;
      }

      private static function rightPad(source:String, targetLength:int, padChar:String = " "):String
      {
        while (source.length < targetLength)
          source += padChar;

        return source;
      }
  }
}
